package netTracker;

import java.util.*;

import netTracker.network.*;
import netTrackerGUI.*;

/**
 * NetTracker is a networked tracker. A tracker is a music production
 * application. It contains a number of tracks, each of which has some number of
 * cells. In each cell, notes can be placed. Upon playback, a horizontal line
 * moves from top to bottom, playing the notes in each cell as it reaches them.
 * Each pattern may contain a certain grouping of cells, and multiple patterns
 * can be created and sequenced to construct a song.
 * 
 * NetTracker has two modes, playback and record. These modes are not mutually
 * exclusive. When in record mode, the can enter notes into cells via the
 * keyboard. The user can load .wav files as samples to use as instruments.
 * NetTracker will adjust the pitch of the samples based on the notes entered by
 * the user.
 * 
 * The unique feature of NetTracker is that it allows concurrent edits by
 * multiple users, allowing networked collaboration on one musical file. When a
 * user creates or loads a file, that user becomes a server. Other clients can
 * connect to this server and send edit requests.
 * 
 * Edit requests that require synchronization are:
 * <ul>
 * <li>adding, changing or removing notes from cells
 * <li>adding, removing or editing patterns
 * <li>loading, removing or renaming samples
 * <li>saving the song
 * <li>changing the BPM or track name
 * <li>changing the cursor position (although each user has an independent
 * cursor, it is possible for cursors to overlap on the same cell)
 * </ul>
 * 
 * These requests should enforce mutual exclusion and be processed in the order
 * received. Other operations are independent, and do not require concurrency,
 * such as playback.
 * 
 * @author David Cummings
 */

public class NetTracker {
	/** Utility class with debug flag: */
	Util util = new Util(this, true);

	public final static int MAX_SERVERS = 16;
	
	/** XXX For testing only: used to measure latency of client movement requests. */
	public static long clientStartTime;

	/** The GUI associated with this NetTracker. */
	NetTrackerGUI netTrackerGUI;
	/** The currently loaded song. Only one song may be loaded at at time. */
	Song song;
	/** A link to the cell that is currently selected. */
	Cell selectedCell;
	/**
	 * The pattern currently being edited. Only one pattern may be edited at at
	 * time.
	 */
	Pattern currentPattern;
	/** Indicates whether the application is in playback mode. */
	boolean playing;
	/** Indicates whether the application is in recording mode. */
	boolean recording;
	/** Indicates whether locking is on or off. */
	boolean locking;
	/** The port on which to connect. */
	int port;
	/** A lock for letting playback threads sleep. */
	Object playbackLock;
	/** A thread to conduct playback threads. */
	MaestroThread maestroThread;
	/** Engines for handling audio. One for each track. */
	LinkedList<PlaybackEngine> playbackEngines;

	/**
	 * Keeps track of if this instance of NetTracker is the server for a
	 * particular session.
	 */
	boolean isServer;
	/**
	 * Keeps track of if this instance of NetTracker has connected. Note that an
	 * instance must disconnect before it can reconnect to avoid an
	 * "Address already in use" exception.
	 */
	boolean connected;

	/** An object for sending and receiving messages. */
	PostOffice pobox;
	/** The address of this NetTracker instance. */
	Address localAddress;
	/** The address of the NetTracker acting as server for this song. */
	Address sessionServer;
	/** A hash map of servers. */
	HashMap<Address, Integer> serverList;

	/**
	 * Constructs a NetTracker with a default song.
	 * 
	 * @param tracks
	 *            number of tracks
	 * @param cells
	 *            number of cells in each track
	 */
	public NetTracker(NetTrackerGUI netTrackerGUI, int tracks, int cells) {
		this.netTrackerGUI = netTrackerGUI;
		locking = true;
		connected = false;
		serverList = new HashMap<Address, Integer>();
		playbackLock = new Object();
		maestroThread = new MaestroThread(playbackLock, this);
		song = new Song(tracks, cells, this);
		currentPattern = song.getPattern(0);
		
		playbackEngines = new LinkedList<PlaybackEngine>();
		for(int i=0; i<tracks; i++)	// Add a playback engine for each track
			playbackEngines.add(new PlaybackEngine());
		
		new Thread(maestroThread).start();
	}

	/**
	 * Converts an address book to an address array.
	 * 
	 * @param addrBook
	 *            the address book to convert
	 * @return the string array for the addresses
	 */
	private Address[] addressBookToArray(HashMap<Address, Integer> addrBook) {
		Object[] myAddrs = addrBook.keySet().toArray();
		Address[] returnAddresses = new Address[myAddrs.length];
		for (int i = 0; i < returnAddresses.length; i++) {
			returnAddresses[i] = (Address) myAddrs[i];
		}
		return returnAddresses;
	}

	/**
	 * add Converts an address book to a string array.
	 * 
	 * @param addrBook
	 *            the address book to convert
	 * @return the string array for the addresses
	 */
	@SuppressWarnings("unused")
	private String[] addressBookToString(HashMap<Address, Integer> addrBook) {
		Object[] myAddrs = addrBook.keySet().toArray();
		String[] returnAddresses = new String[myAddrs.length];
		for (int i = 0; i < returnAddresses.length; i++) {
			returnAddresses[i] = ((Address) myAddrs[i]).toString();
		}
		return returnAddresses;
	}

	/**
	 * Tries to select a cell with a given set of coordinates.
	 * 
	 * @param cellX
	 * @param cellY
	 * @return false if cell selection failed.
	 */
	public synchronized boolean attemptSelectCell(int cellX, int cellY) {
		System.out.println("Trying to select cell " + cellX + "," + cellY);
		CellGUI target = netTrackerGUI.getTrackerPanel().getCell(cellX, cellY);
		new Thread(new CellSelectorThread(netTrackerGUI.getTrackerPanel(),
				target)).start();
		return false;
	}

	/**
	 * Becomes a session server by notifying the NameServer. Does nothing if
	 * NetTracker is not connected.
	 */
	public void becomeServer() {
		try {
			if (connected) {
				isServer = true;
				localAddress.setServer(true);
				/** Send a JOIN request to update entry in NameServer. */
				Message m = new Message("JOIN", localAddress);
				sendMsg(m, NameServer.NAMESERVER_ADDRESS);
				try {
					pobox.receive(); // consume the reply message.
				} catch (InterruptedException x) {
					util.debug(x);
				}
				/**
				 * Add message handler to handle session server related
				 * messages:
				 */
				ServerMessageHandler serverMessageHandler = new ServerMessageHandler(
						this);
				pobox.addHandler("SELECTCELL", serverMessageHandler);
				pobox.addHandler("MOVE", serverMessageHandler);
				pobox.addHandler("JOINSESSION", serverMessageHandler);
				System.out.println("Registered as session server.");
			} else {
				util.debug("Error: not connected to name server.");
			} // end if (connected)
		} catch (PostalException x) {
			util.debug("Error starting server: " + x);
		} // end try
	} // end becomeServerServerMessageHandler

	public synchronized void broadcastCellSelection() {
		Message m = new Message("OUTLINE", localAddress.getID(), localAddress);
		// Broadcast new location of outline
		m.setMsg(netTrackerGUI.getTrackerPanel().getSelectedCell().toString());
		try {
			pobox.broadcast(m);
		} catch (PostalException x) {
			util.debug("Error broadcasting OUTLINE message: " + x);
		}
	}

	/**
	 * Connects to the network by creating local addresses and connecting to the
	 * NameServer to get a unique ID. Does nothing if NetTracker is already
	 * connected.
	 * 
	 * @param port
	 *            the port to connect on
	 */
	public void connect(int port) {
		this.port = port;
		if (!connected) {
			/**
			 * Create addresses and connects to a NameServer to get a unique ID.
			 */
			localAddress = new Address(port);
			try {
				pobox = new PostOffice(localAddress);
				AddressFactory myAddrFactory = new AddressFactory(pobox);
				localAddress = myAddrFactory.makeAddress(localAddress);
				pobox.setLocalAddress(localAddress);
				System.out.println("Joined name server. Our address: "
						+ localAddress);
			} catch (PostalException e) {
				util.debug(e);
			}
			GlobalMessageHandler globalMessageHandler = new GlobalMessageHandler(this);
			pobox.addHandler("NOTE", globalMessageHandler);
			connected = true;
		}
	}

	/**
	 * Retrieves a request cell from the current pattern. Note that the
	 * coordinate (0,0) represents the top-left cell.
	 * 
	 * @param x
	 *            The track to retrieve the cell from.
	 * @param y
	 *            The cell to retrieve.
	 * @return
	 */
	public Cell getCell(int x, int y) {
		return currentPattern.getTracks()[x].getCells()[y];
	}

	/**
	 * Retrieves a request cell. Note that the coordinate (0,0) represents the
	 * top-left cell.
	 * 
	 * @param pattern
	 *            The pattern to get the cell from.
	 * @param x
	 *            The track to retrieve the cell from.
	 * @param y
	 *            The cell to retrieve.
	 * @return
	 */
	public Cell getCell(int pattern, int x, int y) {
		return song.getPattern(pattern).getTracks()[x].getCells()[y];
	}

	public NetTrackerGUI getGUI() {
		return netTrackerGUI;
	}

	/**
	 * @return the id of this process.
	 */
	public int getID() {
		return localAddress.getID();
	}

	public Address getLocalAddress() {
		return localAddress;
	}

	/** @return the number of cells in the song. */
	public int getNumCells() {
		return song.getNumCells();
	}

	/** @return the number of tracks in the song. */
	public int getNumTracks() {
		return song.getNumTracks();
	}

	public PostOffice getPO() {
		return pobox;
	}

	/**
	 * Retrieves a list of session servers from the name server
	 * 
	 * @return an array of addresses of session servers
	 * @throws PostalException
	 * @throws InterruptedException
	 */
	public Address[] getServerList() throws PostalException,
			InterruptedException {
		if (localAddress == null)
			return null;
		Message m = new Message("LISTSERVERS", localAddress);
		sendMsg(m, NameServer.NAMESERVER_ADDRESS);
		m = pobox.receive();
		serverList = m.getAddressBook();
		return addressBookToArray(serverList);
	}

	/** @return the song currently loaded in the tracker. */
	public Song getSong() {
		return song;
	}

	/** @return the array of tracks in the current pattern. */
	public Track[] getTracks() {
		return currentPattern.getTracks();
	}

	/**
	 * Informs a client of the status of a song by sending them the song as a
	 * message.
	 * 
	 * @param src
	 *            the client address
	 */
	public synchronized void informClient(Address src) {
		try {
			Message m = new Message("SONGDATA", localAddress);
			m.setSong(song);
			pobox.send(m, src);
			/** Get the set of all outline IDs: */
			Set<Integer> s = netTrackerGUI.getTrackerPanel().getOutlinedCells().keySet();
			Object[] outlinedIDs = s.toArray();
			/** Send an outline message for each outlined cell. */
			for(Object id:outlinedIDs){
				int i = (Integer)id;
				m = new Message("OUTLINE", i, localAddress);
				m.setMsg(getOutlinedCellString(i));
				pobox.send(m, src);
			}
			CellGUI selectedCell = netTrackerGUI.getTrackerPanel().getSelectedCell();
			if(selectedCell != null){
				m = new Message("OUTLINE", localAddress.getID(), localAddress);
				m.setMsg(selectedCell.toString());
				pobox.send(m, src);
			}
		} catch (PostalException x) {
			util.debug("Error sending song data: " + x);
		}
	}
	
	public String getOutlinedCellString(int id){
		return netTrackerGUI.getTrackerPanel().getOutlinedCell(id).toString();
	}

	/**
	 * @return true if locking is on, false otherwise.
	 */
	public boolean isLocking() {
		return locking;
	}

	/**
	 * @return true if this NetTracker is acting as a server.
	 */
	public boolean isServer() {
		return isServer;
	}

	/**
	 * Connects to a NetTracker server session.
	 * 
	 * @param target
	 *            the address of the target to connect to.
	 */
	public void joinServer(Address target) {
		if (localAddress.equals(target)) // Cannot join own server
			return;
		sessionServer = target;
		if (isServer) { // If already a server, cancel servitude:
			isServer = false;
			localAddress.setServer(false);
			try { // Update status with NameServer.
				pobox.send(new Message("JOIN", localAddress),
						NameServer.NAMESERVER_ADDRESS);
				localAddress = pobox.receive().getOtherAddr();
			} catch (Exception x) {
				util.debug("Error canceling servitude: " + x);
			}
		}
		ClientMessageHandler clientMessageHandler = new ClientMessageHandler(this);
		pobox.addHandler("OUTLINE", clientMessageHandler);
		pobox.addHandler("SONGDATA", clientMessageHandler);
		pobox.addAddress(sessionServer); // Add session server to address book
		try {
			pobox.send(new Message("JOINSESSION", localAddress), sessionServer); 
		} catch (PostalException x) {
			util.debug(x);
		}
		System.out.println("Joined server: " + target);
	}

	/**
	 * Tries to move to a cell to outline and broadcasts the results. Analogous
	 * to selectOutline. Method for session servers only.
	 * 
	 * @param outlineID
	 *            the ID of the node outlining the cell.
	 * @param selectX
	 *            the x coordinate to outline
	 * @param selectY
	 *            the y coordinate to outline
	 */
	public synchronized void moveOutline(int outlineID, int moveX, int moveY) {
		CellOutliner myOutliner = new CellOutliner(netTrackerGUI.getTrackerPanel(), 
				moveX, moveY, outlineID);
		myOutliner.attemptOutlineCell();
		if (myOutliner.wasSuccessful()) {
			Message m = new Message("OUTLINE", outlineID, localAddress);
			// Broadcast new location of outline
			m.setMsg(netTrackerGUI.getTrackerPanel().getOutlinedCell(outlineID)
					.toString());
			try {
				pobox.broadcast(m);
			} catch (PostalException x) {
				util.debug("Error broadcasting OUTLINE message: " + x);
			}
		}
	}

	/**
	 * Outlines a cell for the given ID or selects the cell if the ID matches
	 * this node's ID. Method for session clients only.
	 * 
	 * @param outlineID
	 *            the ID of the node outlining the cell
	 * @param selectX
	 *            the x coordinate to outline
	 * @param selectY
	 *            the y coordinate to outline
	 */
	public synchronized void outline(int outlineID, int selectX, int selectY) {
		if (outlineID == localAddress.getID()) {
			selectCell(selectX, selectY);
			System.out.println("Client time to select: "+(System.nanoTime()-clientStartTime));
		} else {
			CellGUI current = netTrackerGUI.getTrackerPanel().getOutlinedCell(
					outlineID);
			if (current != null)
				current.unoutline(outlineID);
			current = netTrackerGUI.getTrackerPanel().getCell(selectX, selectY);
			current.outline(outlineID);
			netTrackerGUI.getTrackerPanel().setOutlinedCell(current, outlineID);
		}
	}

	/**
	 * Selects a cell with a given set of coordinates unquestioningly. Should
	 * only be called after checking lock somehow.
	 * 
	 * @param cellX
	 * @param cellY
	 */
	public synchronized void selectCell(int selectX, int selectY) {
		CellGUI current = netTrackerGUI.getTrackerPanel().getSelectedCell();
		if (current != null)
			current.deselect();
		current = netTrackerGUI.getTrackerPanel().getCell(selectX, selectY);
		current.select();
		netTrackerGUI.getTrackerPanel().setSelectedCell(current);
	}

	/**
	 * Tries to select a cell to outline and broadcasts the results. Method for
	 * session servers only.
	 * 
	 * @param outlineID
	 *            the ID of the node outlining the cell.
	 * @param selectX
	 *            the x coordinate to outline.
	 * @param selectY
	 *            the y coordinate to outline.
	 */
	public synchronized void selectOutline(int outlineID, int selectX,
			int selectY) {
		CellGUI targetCell = netTrackerGUI.getCell(selectX, selectY);
		CellOutliner myOutliner = new CellOutliner(netTrackerGUI
				.getTrackerPanel(), targetCell, outlineID);
		myOutliner.attemptOutlineCell();
		if (myOutliner.wasSuccessful()) {
			Message m = new Message("OUTLINE", outlineID, localAddress);
			m.setMsg(selectX + "," + selectY);
			try {
				pobox.broadcast(m);
			} catch (PostalException x) {
				util.debug("Error broadcasting OUTLINE message: " + x);
			}
		}
	}

	/**
	 * Asks the post office to send a message to the session server.
	 * 
	 * @param m
	 *            the message to send.
	 */
	public void sendMsg(Message m) throws PostalException {
		if (isServer)
			throw new PostalException(
					"Error: cannot send server request to self.");
		else if (sessionServer == null)
			throw new PostalException("Error: no link to server.");
		else {
			pobox.send(m, sessionServer);
			// System.out.println("Sent "+m+" to"+sessionServer);
		}
	}

	/**
	 * Asks the post office to send a message to a given destination.
	 * 
	 * @param m
	 *            the message to send.
	 */
	public void sendMsg(Message m, Address dest) throws PostalException {
		pobox.send(m, dest);
	}

	/**
	 * Turns locking on or off.
	 * 
	 * @param lockState
	 *            the state to set the lock to.
	 */
	public void setLocking(boolean lockState) {
		locking = lockState;
	}

	public void setSong(Song song) {
		this.song = song;
	}

	/**
	 * Gets the tempo of the current song.
	 * @return the tempo of the current song.
	 */
	public double getTempo() {
		return song.getTempo();
	}
	
	/**
	 * Tells all tracks to play.
	 */
	public void play() {
		maestroThread.play();
	}

	/**
	 * Tells all tracks to stop.
	 */
	public void stop() {
		maestroThread.stop();
	}

	/**
	 * Broadcasts the entry of a note to all other applications.
	 * @param noteName the name of the note entered
	 * @param location the location of the note
	 */
	public void broadcastNoteEntry(Note newNote, String location) {
		Message m = new Message("NOTE", localAddress);
		m.setMsg(location);
		m.setNote(newNote);
		try {
			pobox.broadcast(m);
		} catch (PostalException x) {
			util.debug("Error broadcasting NOTE message: " + x);
		}
	}

	public void addNote(Note note, int cellX, int cellY) {
		currentPattern.getTracks()[cellX].cells[cellY].setNote(note);
		netTrackerGUI.repaint();
	}

	public void clearCurrentPattern() {
		currentPattern.clear();
		netTrackerGUI.repaint();
	}
	

	public Object getPlaybackLock() {
		return playbackLock;
	}

	public MaestroThread getMaestroThread() {
		return maestroThread;
	}

	/**
	 * Sets an entire row of cells to be highlighted or not highlighted.
	 * @param currentRow the row to set as highlighted or not
	 * @param isHighlighted the status of highlighted to set
	 */
	public void highlightRow(int currentRow, boolean isHighlighted) {
		for(int i=0; i<getNumTracks(); i++){
			getCell(i, currentRow).setHighlighted(isHighlighted);
			netTrackerGUI.getCell(i, currentRow).repaint();
		}
	}

	public PlaybackEngine getPlaybackEngine(int index) {
		return playbackEngines.get(index);
	}
}